/*
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.devtools.j2objc.gen;

import com.google.devtools.j2objc.GenerationTest;
import com.google.devtools.j2objc.Options.MemoryManagementOption;
import com.google.devtools.j2objc.ast.Statement;
import java.io.IOException;
import java.util.List;

/**
 * Tests for {@link StatementGenerator}.
 *
 * @author Tom Ball
 */
public class StatementGeneratorTest extends GenerationTest {

  // TODO(tball): use text block when minimum Java is 15 or higher.
  @SuppressWarnings("StringConcatToTextBlock")
  public static final String SIMPLE_SWITCH_EXPRESSION =
      """
      class Test {
        String howMany(int k) {
          return switch (k) {
            case  1 -> "one";
            case  2 -> "two";
            default -> "many";
          };
        }
      }
      """;

  @SuppressWarnings("StringConcatToTextBlock")
  public static final String SIMPLE_SWITCH_EXPRESSION_WITH_PATTERN_AND_GUARD =
      """
      class Test {
        String test(String str) {
          String msg = switch (str) {
            case String s when s.length() > 10 -> "Long string: " + s;
            case null -> "null";
            case String s -> "Short string: " + s;
          };
          return msg;
        }
      }
      """;

  // Verify that return statements output correctly for reserved words.
  public void testReturnReservedWord() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              static final String BOOL = "bool";
              String test() {
                return BOOL;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return Test_BOOL;");
  }

  // Verify that both a class and interface type invoke getClass() correctly.
  public void testGetClass() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class A {
              void test(ArrayList one, List two) {
                Class<?> classOne = one.getClass();
                Class<?> classTwo = two.getClass();
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "[((JavaUtilArrayList *) nil_chk(one)) java_getClass]");
    assertInTranslation(translation, "[((id<JavaUtilList>) nil_chk(two)) java_getClass]");
  }

  public void testEnumConstantReferences() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              static enum B {
                ONE,
                TWO;
                public static B doSomething(boolean b) {
                  return b ? ONE : TWO;
                }
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "return b ? JreEnum(A_B, ONE) : JreEnum(A_B, TWO);");
  }

  public void testInnerClassFQN() throws IOException {
    String translation =
        translateSourceFile(
            """
            package com.example.foo;
            public class Foo {
              static class Inner {
                public static void doSomething() {}
              }
            }
            class Bar {
              public static void mumber() {
                Foo.Inner.doSomething();
              }
            }
            """,
            "Foo",
            "com/example/foo/Foo.m");
    assertInTranslation(translation, "ComExampleFooFoo_Inner_doSomething();");
  }

  public void testLocalVariableTranslation() throws IOException {
    String source = "Exception e;";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("JavaLangException *e;", result);
  }

  public void testClassCreationTranslation() throws IOException {
    String source = "new Exception(\"test\");";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("create_JavaLangException_initWithNSString_(@\"test\");", result);
  }

  public void testParameterTranslation() throws IOException {
    String source =
        """
        Throwable cause = new Throwable();
        new Exception(cause);
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals("create_JavaLangException_initWithJavaLangThrowable_(cause);", result);
  }

  public void testCastTranslation() throws IOException {
    String source =
        """
        Object o = new Object();
        Throwable t = (Throwable) o;
        int[] i = (int[]) o;
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(3, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals(
        "JavaLangThrowable *t = (JavaLangThrowable *) cast_chk(o, [JavaLangThrowable class]);",
        result);
    result = generateStatement(stmts.get(2));
    assertEquals("IOSIntArray *i = (IOSIntArray *) cast_chk(o, [IOSIntArray class]);", result);
  }

  public void testInterfaceCastTranslation() throws IOException {
    String source =
        """
        java.util.Collection al = new java.util.ArrayList();
        java.util.List l = (java.util.List) al;
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals(
        "id<JavaUtilList> l = (id<JavaUtilList>) cast_check(al, JavaUtilList_class_());", result);
  }

  public void testCatchTranslation() throws IOException {
    String source = "try { ; } catch (Exception e) {}";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("@try {\n;\n}\n@catch (JavaLangException *e) {\n}", result);
  }

  public void testInstanceOfTranslation() throws IOException {
    String source =
        """
        Exception e = new Exception();
        if (e instanceof Throwable) {}
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals("if ([e isKindOfClass:[JavaLangThrowable class]]) {\n}", result);
  }

  public void testFullyQualifiedTypeTranslation() throws IOException {
    String source = "java.lang.Exception e = null;";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("JavaLangException *e = nil;", result);
  }

  public void testToStringRenaming() throws IOException {
    String source =
        """
        Object o = new Object();
        o.toString();
        toString();
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(3, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals("[o description];", result);
    result = generateStatement(stmts.get(2));
    assertEquals("[self description];", result);
  }

  public void testSuperToStringRenaming() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              public String toString() {
                return super.toString();
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "return [super description];");
  }

  public void testAccessPublicConstant() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              public static final int FOO = 1;
              int foo;
              {
                foo = FOO;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "foo_ = Example_FOO;");
  }

  public void testAccessPublicConstant2() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              public static final int FOO = 1;
              int test() {
                int foo = FOO;
                return foo;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "foo = Example_FOO;");
  }

  public void testAccessPrivateConstant() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              private static final int FOO = 1;
              int test() {
                int foo = FOO;
                return foo;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "foo = Example_FOO;");
  }

  public void testAccessExternalConstant() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              static class Bar {
                public static final int FOO = 1;
              }
              int foo;
              {
                foo = Bar.FOO;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "foo_ = Example_Bar_FOO;");
    assertFalse(translation.contains("int Example_Bar_FOO_ = 1;"));
    translation = getTranslatedFile("Example.h");
    assertInTranslation(translation, "#define Example_Bar_FOO 1");
  }

  public void testAccessExternalStringConstant() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              static class Bar {
                public static final String FOO = "Mumble";
              }
              String foo;
              {
                foo = Bar.FOO;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "JreStrongAssign(&self->foo_, Example_Bar_FOO)");
    assertInTranslation(translation, "NSString *Example_Bar_FOO = @\"Mumble\";");
    translation = getTranslatedFile("Example.h");
    assertInTranslation(translation, "FOUNDATION_EXPORT NSString *Example_Bar_FOO;");
    assertInTranslation(translation, "J2OBJC_STATIC_FIELD_OBJ_FINAL(Example_Bar, FOO, NSString *)");
  }

  public void testObjectDeclaration() throws IOException {
    List<Statement> stmts = translateStatements("Object o;");
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("id o;", result);
  }

  public void testStaticBooleanFields() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              Boolean b1 = Boolean.TRUE;
              Boolean b2 = Boolean.FALSE;
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(
        translation, "JreStrongAssign(&self->b1_, JreLoadStatic(JavaLangBoolean, TRUE))");
    assertInTranslation(
        translation, "JreStrongAssign(&self->b2_, JreLoadStatic(JavaLangBoolean, FALSE))");
  }

  public void testStringConcatenation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example<K, V> {
              void test() {
                String s = "hello, " + "world";
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "NSString *s = @\"hello, world\"");
  }

  public void testStringConcatenation2() throws IOException {
    String source =
        """
        class A {
          private static final String A = "bob";
          private static final char SPACE = ' ';
          private static final double ANSWER = 22.0 / 2;
          private static final boolean B = false;
          private static final String C = "hello " + A + ' ' + 3 + SPACE + true + ' ' + ANSWER + ' ' + B;
        }
        """;
    String translation = translateSourceFile(source, "A", "A.m");
    assertInTranslation(translation, "\"hello bob 3 true 11.0 false\"");
  }

  public void testStringConcatenationTypes() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example<K, V> {
              Object obj;
              boolean b;
              char c;
              double d;
              float f;
              int i;
              long l;
              short s;
              String str;
              public String toString() {
                return "obj=" + obj + " b=" + b + " c=" + c + " d=" + d + " f=" + f + " i=" + i + " l=" + l + " s=" + s;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(
        translation,
        "return JreStrcat(\"$@$Z$C$D$F$I$J$S\", @\"obj=\", obj_, @\" b=\", b_, @\" c=\", c_,"
            + " @\" d=\", d_, @\" f=\", f_, @\" i=\", i_, @\" l=\", l_, @\" s=\", s_);");
  }

  public void testStringConcatenationWithLiterals() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example<K, V> {
              public String toString() {
                return "literals: " + true + ", " + 'c' + ", " + 1.0d + ", " + 3.14 + ", " + 42 + ", " + 123L + ", " + 1;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "return @\"literals: true, c, 1.0, 3.14, 42, 123, 1\";");
  }

  public void testStringConcatenationEscaping() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example<K, V> {
              String s = "hello, " + 50 + "% of the world\\n";
            }
            """,
            "Example", "Example.m");
    assertInTranslation(
        translation, "JreStrongAssign(&self->s_, @\"hello, 50% of the world\\n\");");
  }

  public void testStringConcatenationMethodInvocation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              String getStr() {
                return "str";
              }
              int getInt() {
                return 42;
              }
              void test() {
                String a = "foo" + getStr() + "bar" + getInt() + "baz";
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "JreStrcat(\"$$$I$\", @\"foo\", [self getStr], @\"bar\", [self getInt], @\"baz\")");
  }

  public void testVarargsMethodInvocation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              public void call() {
                foo(null);
                bar("", null, null);
              }
              void foo(Object... args) {}
              void bar(String firstArg, Object... varArgs) {}
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "[self fooWithNSObjectArray:");
    assertInTranslation(
        translation,
        "[IOSObjectArray arrayWithObjects:(id[]){ nil, nil } count:2 type:NSObject_class_()]");
    assertInTranslation(translation, "[self barWithNSString:");
    assertInTranslation(translation, "withNSObjectArray:");
  }

  public void testVarargsMethodInvocationSingleArg() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              public void call() {
                foo(1);
              }
              void foo(Object... args) {}
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(
        translation,
        "[self fooWithNSObjectArray:[IOSObjectArray arrayWithObjects:(id[]){"
            + " JavaLangInteger_valueOfWithInt_(1) } count:1 type:NSObject_class_()]];");
  }

  public void testVarargsMethodInvocationPrimitiveArgs() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              void call() {
                foo(1);
              }
              void foo(int... i) {}
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "[self fooWithIntArray:[IOSIntArray arrayWithInts:(int32_t[]){ 1 } count:1]];");
  }


  public void testStaticInnerSubclassAccessingOuterStaticVar() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public static final Object FOO = new Object();
              static class Inner {
                Object test() {
                  return FOO;
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return JreLoadStatic(Test, FOO);");
  }

  public void testStaticInnerSubclassAccessingOuterStaticVarStrictFieldLoad() throws IOException {
    options.setStrictFieldLoad(true);
    String translation =
        translateSourceFile(
            """
            public class Test {
              public static final Object FOO = new Object();
              static class Inner {
                Object test() {
                  return FOO;
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "return JreStrictFieldStrongLoad(JreLoadStaticRef(Test, FOO));");
  }

  public void testReservedIdentifierReference() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public int test(int id) {
                return id;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "- (int32_t)testWithInt:(int32_t)id_");
    assertInTranslation(translation, "return id_;");
  }

  public void testReservedTypeQualifierReference() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public int test(int in, int out) {
                return in + out;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "- (int32_t)testWithInt:(int32_t)inArg");
    assertInTranslation(translation, "return inArg + outArg;");
  }

  public void testFieldAccess() throws IOException {
    String translation =
        translateSourceFile(
            """
            import com.google.j2objc.annotations.Weak;
            public class Test {
              Object i;
              @Weak
              Object j;
              Test(Object otherI, Object otherJ) {
                i = otherI;
                j = otherJ;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "j_ = otherJ;");
    assertInTranslation(translation, "RELEASE_(i_);");
  }

  public void testFieldAccessStrictField() throws IOException {
    options.setStrictFieldAssign(true);
    String translation =
        translateSourceFile(
            """
            import com.google.j2objc.annotations.Weak;
            public class Test {
              Object i;
              @Weak
              Object j;
              Test(Object otherI, Object otherJ) {
                i = otherI;
                j = otherJ;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "JreStrictFieldStrongAssign(&self->i_, otherI);");
    assertInTranslation(translation, "j_ = otherJ;");
    assertInTranslation(translation, "JreStrictFieldStrongRelease(&i_);");
  }

  public void testStaticFinalFieldAccessWithParenthesizedExpression() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              private static final int t = 7;
              static int test() {
                Object o = new Test();
                return ((Test) o).t;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return Test_t;");
  }

  public void testInnerInnerClassFieldAccess() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              static class One {}
              static class Two extends Test {
                Integer i;
                Two(Integer i) {
                  this.i = i;
                }
                int getI() {
                  return i.intValue();
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "- (instancetype)initWithJavaLangInteger:(JavaLangInteger *)i {");
    assertInTranslation(translation, "return [((JavaLangInteger *) nil_chk(i_)) intValue];");
  }

  public void testInnerInnerClassFieldAccessStrictField() throws IOException {
    options.setStrictFieldLoad(true);
    String translation =
        translateSourceFile(
            """
            public class Test {
              static class One {}
              static class Two extends Test {
                Integer i;
                Two(Integer i) {
                  this.i = i;
                }
                int getI() {
                  return i.intValue();
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "- (instancetype)initWithJavaLangInteger:(JavaLangInteger *)i {");
    assertInTranslation(
        translation,
        "return [((JavaLangInteger *) nil_chk(JreStrictFieldStrongLoad(&i_))) intValue];");
  }

  public void testInnerClassSuperConstructor() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              static class One {
                int i;
                One(int i) {
                  this.i = i;
                }
              }
              static class Two extends One {
                Two(int i) {
                  super(i);
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "- (instancetype)initWithInt:(int32_t)i");
    assertTranslatedLines(
        translation,
        """
        void Test_Two_initWithInt_(Test_Two *self, int32_t i) {
          Test_One_initWithInt_(self, i);
        }
        """);
  }

  public void testStaticInnerClassSuperFieldAccess() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              protected int foo;
              static class One extends Test {
                int i;
                One() {
                  i = foo;
                }
                int test() {
                  return i;
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "- (instancetype)init {");
    assertInTranslation(translation, "self->i_ = self->foo_;");
    assertInTranslation(translation, "return i_;");
  }

  public void testMethodInvocationOfReturnedInterface() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class Test<K, V> {
              Iterator<Map.Entry<K, V>> iterator;
              K test() {
                return iterator.next().getKey();
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "return [((id<JavaUtilMap_Entry>) "
            + "nil_chk([((id<JavaUtilIterator>) nil_chk(iterator_)) next])) getKey];");
  }

  public void testAnonymousClassInInnerStatic() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class Test {
              static <T> Enumeration<T> enumeration(Collection<T> collection) {
                final Collection<T> c = collection;
                return new Enumeration<T>() {
                  Iterator<T> it = c.iterator();
                  public boolean hasMoreElements() {
                    return it.hasNext();
                  }
                  public T nextElement() {
                    return it.next();
                  }
                };
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return [((id<JavaUtilIterator>) nil_chk(it_)) hasNext];");
    assertInTranslation(
        translation, "return JreRetainedLocalValue([((id<JavaUtilIterator>) nil_chk(it_)) next]);");
    assertFalse(translation.contains("Test *this$0;"));
  }

  public void testGenericMethodWithAnonymousReturn() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class Test {
              public static <T> Enumeration<T> enumeration(final Collection<T> collection) {
                return new Enumeration<T>() {
                  Iterator<T> it = collection.iterator();
                  public boolean hasMoreElements() {
                    return it.hasNext();
                  }
                  public T nextElement() {
                    return it.next();
                  }
                };
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "return create_Test_1_initWithJavaUtilCollection_(collection);");
    assertInTranslation(
        translation,
        "- (instancetype)initWithJavaUtilCollection:(id<JavaUtilCollection>)capture$0;");
    assertInTranslation(
        translation,
        "__attribute__((unused)) static Test_1 *new_Test_1_initWithJavaUtilCollection_("
            + "id<JavaUtilCollection> capture$0) NS_RETURNS_RETAINED;");
  }

  public void testEnumInEqualsTest() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              enum TicTacToe {
                X,
                Y
              }
              boolean isX(TicTacToe ttt) {
                return ttt == TicTacToe.X;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return ttt == JreLoadEnum(Test_TicTacToe, X);");
  }

  public void testArrayLocalVariable() throws IOException {
    String source = "char[] array = new char[1];";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("IOSCharArray *array = [IOSCharArray arrayWithLength:1];", result);

    source = "char array[] = new char[1];";
    stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    result = generateStatement(stmts.get(0));
    assertEquals("IOSCharArray *array = [IOSCharArray arrayWithLength:1];", result);
  }

  public void testArrayParameterLengthUse() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test(char[] foo, char bar[]) {
                sync(foo.length, bar.length);
              }
              void sync(int a, int b) {}
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "[self syncWithInt:((IOSCharArray *) nil_chk(foo))->size_ withInt:"
            + "((IOSCharArray *) nil_chk(bar))->size_];");
  }

  public void testLongLiteral() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public static void testLong() {
                long l1 = 1L;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "int64_t l1 = 1LL");
  }

  public void testStringLiteralEscaping() throws IOException {
    String source = "String s = \"\\u1234\\u2345\\n\";";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("NSString *s = @\"\\u1234\\u2345\\n\";", result);
  }

  public void testStringLiteralEscapingInStringConcatenation() throws IOException {
    String source = "String s = \"\\u1234\" + \"Hi\" + \"\\u2345\\n\";";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("NSString *s = @\"\\u1234Hi\\u2345\\n\";", result);
  }

  /**
   * Verify that Unicode escape sequences that aren't legal C++ Unicode are converted to C
   * hexadecimal escape sequences. This works because all illegal sequences are less than 0xA0.
   */
  public void testStringLiteralWithInvalidCppUnicode() throws IOException {
    String source = "String s = \"\\u0093\\u0048\\u0069\\u0094\\n\";";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("NSString *s = @\"\\xc2\\x93Hi\\xc2\\x94\\n\";", result);
  }

  public void testArrayInitializer() {
    String source =
        """
        int[] a = { 1, 2, 3 };
        char b[] = { '4', '5' };
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals(
        "IOSIntArray *a = [IOSIntArray arrayWithInts:(int32_t[]){ 1, 2, 3 } count:3];", result);
    result = generateStatement(stmts.get(1));
    assertEquals(
        "IOSCharArray *b = [IOSCharArray arrayWithChars:(unichar[]){ '4', '5' } count:2];", result);
  }

  /** Verify that static array initializers are rewritten as method calls. */
  public void testStaticArrayInitializer() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              static int[] a = {1, 2, 3};
              static char b[] = {'4', '5'};
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "JreStrongAssignAndConsume(&Test_a, "
            + "[IOSIntArray newArrayWithInts:(int32_t[]){ 1, 2, 3 } count:3]);");
    assertInTranslation(
        translation,
        "JreStrongAssignAndConsume(&Test_b, "
            + "[IOSCharArray newArrayWithChars:(unichar[]){ '4', '5' } count:2]);");
  }

  public void testLocalArrayCreation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              char[] test() {
                int high = 0xD800, low = 0xDC00;
                return new char[] {(char) high, (char) low};
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(
        translation,
        "return [IOSCharArray "
            + "arrayWithChars:(unichar[]){ (unichar) high, (unichar) low } count:2];");
  }

  // Regression test: "case:" was output instead of "case".
  public void testSwitchCaseStatement() throws IOException {
    String source = "int c = 1; " + "switch (c) { case 1: c = 0; break; default: break; }";
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertTrue(result.contains("case 1:"));
  }

  public void testEnhancedForStatement() throws IOException {
    String source =
        """
        String[] strings = {"test1", "test2"};
        for (String string : strings) {
        }
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertTranslatedLines(
        result,
        "{",
        "IOSObjectArray *a__ = strings;",
        "NSString * const *b__ = a__->buffer_;",
        "NSString * const *e__ = b__ + a__->size_;",
        "while (b__ < e__) {",
        "NSString *string = RETAIN_AND_AUTORELEASE(*b__++);",
        "}",
        "}");
  }

  public void testEnhancedForStatementInSwitchStatement() throws IOException {
    String source =
        """
        int test = 5;
        int[] myInts = new int[10];
        switch (test) {
          case 0:
            break;
          default:
            for (int i : myInts) {}
            break;
        }
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(3, stmts.size());
    String result = generateStatement(stmts.get(2));
    assertTranslatedLines(
        result,
        """
        switch (test) {
          case 0:
            break;
          default:
            {
              IOSIntArray *a__ = myInts;
              int32_t const *b__ = a__->buffer_;
              int32_t const *e__ = b__ + a__->size_;
              while (b__ < e__) {
                int32_t i = *b__++;
              }
            }
            break;
        }
        """);
  }

  public void testSwitchStatementWithExpression() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              static enum Test {
                ONE,
                TWO
              }
              Test foo() {
                return Test.ONE;
              }
              void bar() {
                switch (foo()) {
                  case ONE:
                    break;
                  case TWO:
                    break;
                }
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "switch ([[self foo] ordinal])");
  }

  public void testClassVariable() throws IOException {
    String source =
        """
        Class<?> myClass = getClass();
        Class<?> mySuperClass = myClass.getSuperclass();
        Class<?> enumClass = Enum.class;\
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(3, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("IOSClass *myClass = [self java_getClass];", result);
    result = generateStatement(stmts.get(1));
    assertEquals("IOSClass *mySuperClass = [myClass getSuperclass];", result);
    result = generateStatement(stmts.get(2));
    assertEquals("IOSClass *enumClass = JavaLangEnum_class_();", result);
  }

  public void testInnerClassCreation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              int x;
              class Inner {
                int y;
                Inner(int i) {
                  y = i + x;
                }
              }
              public Inner test() {
                return this.new Inner(3);
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "return create_A_Inner_initWithA_withInt_(self, 3);");
  }

  public void testNewFieldNotRetained() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class A {
              Map map;
              A() {
                map = new HashMap();
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(
        translation, "JreStrongAssignAndConsume(&self->map_, new_JavaUtilHashMap_init())");
  }

  public void testCreatedFieldNotConsumedStrictFieldAssign() throws IOException {
    options.setStrictFieldAssign(true);
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class A {
              Map map;
              A() {
                map = new HashMap();
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(
        translation, "JreStrictFieldStrongAssign(&self->map_, create_JavaUtilHashMap_init())");
  }

  public void testStringAddOperator() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class A {
              String myString;
              A() {
                myString = "Foo";
                myString += "Bar";
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "JreStrAppendStrong(&self->myString_, \"$\", @\"Bar\");");
  }

  public void testInterfaceStaticVarReference() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public interface I {
                void foo();
                public static final int FOO = 1;
              }
              public class Bar implements I {
                public void foo() {
                  int i = I.FOO;
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "int32_t i = Test_I_FOO;");
  }

  public void testMethodWithPrimitiveArrayParameter() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public void foo(char[] chars) {}
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "fooWithCharArray:");
  }

  public void testMethodWithGenericArrayParameter() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test<T> {
              T[] tArray;
              public void foo(T[] ts) {}
              public void bar(Test<? extends T>[] tLists) {}
              public void foo() {
                foo(tArray);
              }
              public class Inner<S extends Test<T>> {
                public void baz(S[] ss) {}
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "- (void)fooWithNSObjectArray:");
    assertInTranslation(translation, "- (void)barWithTestArray:");
    assertInTranslation(translation, "- (void)foo {");
    assertInTranslation(translation, "[self fooWithNSObjectArray:");
    assertInTranslation(translation, "- (void)bazWithTestArray:");
  }

  public void testGenericMethodWithfGenericArrayParameter() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public <T> void foo(T[] ts) {}
              public void foo() {
                foo(new String[1]);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "[self fooWithNSObjectArray:");
  }

  public void testJreDoubleNegativeInfinity() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public void foo() {
                Double d = Double.NEGATIVE_INFINITY;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "JavaLangDouble_valueOfWithDouble_(JavaLangDouble_NEGATIVE_INFINITY)");
  }

  public void testInvokeMethodInConcreteImplOfGenericInterface() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public interface Foo<T> {
                void foo(T t);
              }
              public class FooImpl implements Foo<Test> {
                public void foo(Test t) {}
                public void bar() {
                  foo(new Test());
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "[self fooWithId:");
  }

  public void testNewStringWithArrayInAnonymousClass() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public Runnable foo() {
                String s1 = new String(new char[10]);
                return new Runnable() {
                  public void run() {
                    String s = new String(new char[10]);
                  }
                };
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "s = [NSString java_stringWith");
  }

  public void testMostNegativeIntegers() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              int min_int = 0x80000000;
              long min_long = 0x8000000000000000L;
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "-0x7fffffff - 1");
    assertInTranslation(translation, "-0x7fffffffffffffffLL - 1");
  }

  public void testInnerNewStatement() throws IOException {
    String translation =
        translateSourceFile(
            """
            class A {
              class B {}
              static B test() {
                return new A().new B();
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "create_A_B_initWithA_(create_A_init())");
  }

  public void testSuperFieldAccess() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              int i;
              class B extends A {
                int i;
                int test() {
                  return super.i + i;
                }
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "return i_ + i_B_;");
  }

  public void testStaticConstants() throws IOException {
    String source =
        """
        float f = Float.NaN;
        double d = Double.POSITIVE_INFINITY;
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(0)).trim();
    assertEquals("float f = JavaLangFloat_NaN;", result);
    result = generateStatement(stmts.get(1)).trim();
    assertEquals("double d = JavaLangDouble_POSITIVE_INFINITY;", result);
  }

  public void testInstanceStaticConstants() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              Foo f;
              void test() {
                int i = f.DEFAULT;
                Object lock = f.LOCK;
              }
            }
            class Foo {
              public static final int DEFAULT = 1;
              public static final Object LOCK = null;
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "int32_t i = Foo_DEFAULT;");
    assertInTranslation(translation, "id lock = JreLoadStatic(Foo, LOCK);");
  }

  public void testCastGenericReturnType() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              static class A<E extends A> {
                E other;
                public E getOther() {
                  return other;
                }
              }
              static class B extends A<B> {
                B other = getOther();
              }
            }
            """,
            "Test",
            "Test.h");
    // Test_B's "other" needs a trailing underscore, since there is an "other"
    // field in its superclass.
    assertInTranslation(translation, "Test_B *other_B_;");
    translation = getTranslatedFile("Test.m");
    assertInTranslation(translation, "JreStrongAssign(&self->other_B_, [self getOther])");
  }

  public void testArrayInstanceOfTranslation() throws IOException {
    String source =
        """
        Object args = new String[0];
        if (args instanceof String[]) {}
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals("if ([IOSClass_arrayType(NSString_class_(), 1) isInstance:args]) {\n}", result);
  }

  public void testInterfaceArrayInstanceOfTranslation() throws IOException {
    String source =
        """
        Object args = new Readable[0];
        if (args instanceof Readable[]) {}
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals(
        "if ([IOSClass_arrayType(JavaLangReadable_class_(), 1) isInstance:args]) {\n}", result);
  }

  public void testPrimitiveArrayInstanceOfTranslation() throws IOException {
    String source =
        """
        Object args = new int[0];
        if (args instanceof int[]) {}
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(2, stmts.size());
    String result = generateStatement(stmts.get(1));
    assertEquals("if ([args isKindOfClass:[IOSIntArray class]]) {\n}", result);
  }

  public void testObjectArrayInitializer() throws IOException {
    String source = "String[] a = { \"one\", \"two\", \"three\" };";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals(
        "IOSObjectArray *a = [IOSObjectArray "
            + "arrayWithObjects:(id[]){ @\"one\", @\"two\", @\"three\" } "
            + "count:3 type:NSString_class_()];",
        result);

    source = "Comparable[] a = { \"one\", \"two\", \"three\" };";
    stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    result = generateStatement(stmts.get(0));
    assertEquals(
        "IOSObjectArray *a = [IOSObjectArray "
            + "arrayWithObjects:(id[]){ @\"one\", @\"two\", @\"three\" } "
            + "count:3 type:JavaLangComparable_class_()];",
        result);
  }

  public void testArrayPlusAssign() throws IOException {
    String source =
        """
        int[] array = new int[] { 1, 2, 3 };
        int offset = 1;
        array[offset] += 23;
        """;
    List<Statement> stmts = translateStatements(source);
    assertEquals(3, stmts.size());
    String result = generateStatement(stmts.get(2));
    assertEquals("*IOSIntArray_GetRef(array, offset) += 23;", result);
  }

  public void testRegisterVariableName() throws IOException {
    String source = "int register = 42;";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String result = generateStatement(stmts.get(0));
    assertEquals("int32_t register_ = 42;", result);
  }

  public void testStaticVariableSetterReference() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              public static java.util.Date today;
            }
            class Test {
              void test(java.util.Date now) {
                Example.today = now;
              }
            }
            """,
            "Example",
            "Example.m");
    assertInTranslation(translation, "JreStrongAssign(JreLoadStaticRef(Example, today), now);");
  }

  // b/5872533: reserved method name not renamed correctly in super invocation.
  public void testSuperReservedName() throws IOException {
    addSourceFile(
        """
        public class A {
          A() {}
          public void init(int a) { }
        }
        """,
        "A.java");
    addSourceFile(
        """
        public class B extends A {
          B() {}
          public void init(int b) {
            super.init(b);
          }
        }
        """,
        "B.java");
    String translation = translateSourceFile("A", "A.h");
    assertInTranslation(translation, "- (instancetype)init;");
    assertInTranslation(translation, "- (void)init__WithInt:(int32_t)a");
    translation = translateSourceFile("B", "B.m");
    assertInTranslation(translation, "A_init(self);");
    assertInTranslation(translation, "[super init__WithInt:b];");
  }

  // b/5872757: verify multi-dimensional array has cast before each
  // secondary reference.
  public void testMultiDimArrayCast() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              static String[][] a = new String[1][1];
              public static void main(String[] args) {
                a[0][0] = "42";
                System.out.println(a[0].length);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "IOSObjectArray_Set(nil_chk(IOSObjectArray_Get(nil_chk(Test_a), 0)), 0, @\"42\");");
    assertInTranslation(
        translation, "((IOSObjectArray *) nil_chk(IOSObjectArray_Get(Test_a, 0)))->size_");
  }

  public void testMultiDimArray() throws IOException {
    String source = "int[][] a = new int[][] { null, { 0, 2 }, { 2, 2 }};";
    List<Statement> stmts = translateStatements(source);
    assertEquals(1, stmts.size());
    String translation = generateStatement(stmts.get(0));
    assertInTranslation(
        translation,
        "IOSObjectArray *a = [IOSObjectArray arrayWithObjects:(id[]){ nil, "
            + "[IOSIntArray arrayWithInts:(int32_t[]){ 0, 2 } count:2], "
            + "[IOSIntArray arrayWithInts:(int32_t[]){ 2, 2 } count:2] } count:3 "
            + "type:IOSClass_intArray(1)];");
  }

  public void testObjectMultiDimArray() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              Integer i = new Integer(1);
              Integer j = new Integer(2);
              void test() {
                Integer[][] a = new Integer[][] {null, {i, j}, {j, i}};
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "IOSObjectArray *a = [IOSObjectArray arrayWithObjects:(id[]){ nil, "
            + "[IOSObjectArray arrayWithObjects:(id[]){ i_, j_ } count:2 "
            + "type:JavaLangInteger_class_()], "
            + "[IOSObjectArray arrayWithObjects:(id[]){ j_, i_ } count:2 "
            + "type:JavaLangInteger_class_()] } count:3 "
            + "type:IOSClass_arrayType(JavaLangInteger_class_(), 1)];");
  }

  public void testVarargsMethodInvocationZeroLengthArray() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Example {
              public void call() {
                foo(new Object[0]);
                bar(new Object[0]);
              }
              public void foo(Object... args) {}
              public void bar(Object[]... args) {}
            }
            """,
            "Example",
            "Example.h");
    assertInTranslation(translation, "- (void)fooWithNSObjectArray:(IOSObjectArray *)args");
    assertInTranslation(translation, "- (void)barWithNSObjectArray2:(IOSObjectArray *)args");
    translation = getTranslatedFile("Example.m");

    // Should be equivalent to foo(new Object[0]).
    assertInTranslation(
        translation,
        "[self fooWithNSObjectArray:[IOSObjectArray arrayWithLength:0 type:NSObject_class_()]]");

    // Should be equivalent to bar(new Object[] { new Object[0] }).
    assertInTranslation(
        translation,
        "[self barWithNSObjectArray2:[IOSObjectArray arrayWithObjects:"
            + "(id[]){ [IOSObjectArray arrayWithLength:0 type:NSObject_class_()] } count:1 "
            + "type:IOSClass_arrayType(NSObject_class_(), 1)]];");
  }

  public void testVarargsIOSMethodInvocation() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.lang.reflect.Constructor;
            public class Test {
              public void test() throws Exception {
                Constructor c1 = Test.class.getConstructor();
                Constructor c2 = Test.class.getConstructor(String.class);
                Constructor c3 = Test.class.getConstructor(String.class, Byte.TYPE);
                Class[] types = new Class[] {Object.class, Exception.class};
                Constructor c4 = Test.class.getConstructor(types);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "c1 = [Test_class_() getConstructor:"
            + "[IOSObjectArray arrayWithLength:0 type:IOSClass_class_()]];");
    assertInTranslation(
        translation,
        "c2 = [Test_class_() getConstructor:[IOSObjectArray "
            + "arrayWithObjects:(id[]){ NSString_class_() } count:1 type:IOSClass_class_()]];");
    assertInTranslation(
        translation,
        "c3 = [Test_class_() getConstructor:[IOSObjectArray arrayWithObjects:"
            + "(id[]){ NSString_class_(), JreLoadStatic(JavaLangByte, TYPE) } count:2 "
            + "type:IOSClass_class_()]];");

    // Array contents should be expanded.
    assertInTranslation(translation, "c4 = [Test_class_() getConstructor:types];");
  }

  public void testGetVarargsWithLeadingParameter() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test() throws Exception {
                getClass().getMethod("equals", Object.class);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "[[self java_getClass] getMethod:@\"equals\" parameterTypes:[IOSObjectArray "
            + "arrayWithObjects:(id[]){ NSObject_class_() } count:1 type:IOSClass_class_()]];");
  }

  public void testGetVarargsWithLeadingParameterNoArgs() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test() throws Exception {
                getClass().getMethod("hashCode", new Class[0]);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "[[self java_getClass] getMethod:@\"hashCode\" parameterTypes:[IOSObjectArray "
            + "arrayWithLength:0 type:IOSClass_class_()]];");
  }

  public void testTypeVariableWithBoundCast() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.ArrayList;
            public class Test {
              public static class Foo<T extends Foo.Bar> {
                public static class Bar {}
                public T foo() {
                  return null;
                }
              }
              public static class BarD extends Foo.Bar {}
              public void bar(Foo<BarD> f) {
                BarD b = f.foo();
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "[((Test_Foo *) nil_chk(f)) foo]");
  }

  // b/5934474: verify that static variables are always referenced by
  // their accessors in functions, since their class may not have loaded.
  public void testFunctionReferencesStaticVariable() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class HelloWorld {
              static String staticString = "hello world";
              public static void main(String[] args) {
                System.out.println(staticString);
              }
            }
            """,
            "HelloWorld",
            "HelloWorld.m");
    assertInTranslation(translation, "printlnWithNSString:HelloWorld_staticString];");
  }

  public void testThisCallInEnumConstructor() throws IOException {
    String translation =
        translateSourceFile(
            """
            public enum Test {
              A,
              B(1);
              private int i;
              private Test(int i) {
                this.i = i;
              }
              private Test() {
                this(0);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "Test_initWithInt_withNSString_withInt_(self, 0, __name, __ordinal);");
  }

  public void testThisCallInInnerConstructor() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              class Inner {
                public Inner() {}
                public Inner(int foo) {
                  this();
                  int i = foo;
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "Test_Inner_initWithTest_(self, outer$);");
  }

  // Verify that an external string can be used in string concatenation,
  // for a parameter to a translated method.
  public void testConcatPublicStaticString() throws IOException {
    String translation =
        translateSourceFile(
            """
            class B {
              public static final String separator = "/";
            }
            public class A {
              String prefix(Object o) {
                return new String(o + B.separator);
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(
        translation, "[NSString stringWithString:JreStrcat(\"@$\", o, B_separator)]");
  }

  public void testStringConcatWithBoolean() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A { String test(boolean b) { return "foo: " + b; }}
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "return JreStrcat(\"$Z\", @\"foo: \", b);");
  }

  public void testStringConcatWithChar() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              String test(char c) {
                return "foo: " + c;
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "return JreStrcat(\"$C\", @\"foo: \", c);");
  }

  // Verify that double quote character constants are concatenated correctly.
  public void testConcatDoubleQuoteChar() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              static final char QUOTE = '"';
              static final String TEST = QUOTE + "";
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "Test_TEST = @\"\\\"\";");
  }

  // Verify that return statements in constructors return self.
  public void testConstructorReturn() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              public A() {
                return;
              }
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "return self;");
  }

  public void testNonAsciiOctalEscapeInString() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              String s1 = "\\177";
              String s2 = "\\200";
              String s3 = "\\377";
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "@\"\\x7f\"");
    assertInTranslation(translation, "@\"\\xc2\\x80\"");
    assertInTranslation(translation, "@\"\\u00ff\"");
  }

  public void testCharLiteralsAreEscaped() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              public static final char APOSTROPHE = '\\'';
              public static final char BACKSLASH = '\\\\';
              void foo(char c) {}
              void test() {
                foo('\\'');
                foo('\\\\');
              }
            }
            """,
            "A",
            "A.h");
    assertInTranslation(translation, "#define A_APOSTROPHE '\\''");
    assertInTranslation(translation, "#define A_BACKSLASH '\\\\'");
    translation = getTranslatedFile("A.m");
    assertInTranslation(translation, "fooWithChar:'\\'']");
    assertInTranslation(translation, "fooWithChar:'\\\\']");
  }

  public void testStaticVarAccessFromInnerClass() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public static String foo;
              interface Assigner {
                void assign(String s);
              }
              static {
                new Assigner() {
                  public void assign(String s) {
                    foo = s;
                  }
                };
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "JreStrongAssign(JreLoadStaticRef(Test, foo), s);");
  }

  public void testNoAutoreleasePoolForStatement() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              public void foo() {
                for (int i = 0; i < 10; i++) {
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(
        translation,
        """
        for (int32_t i = 0; i < 10; i++) {
        }
        """);
  }

  public void testAutoreleasePoolForStatement() throws IOException {
    String translation =
        translateSourceFile(
            """
            import com.google.j2objc.annotations.AutoreleasePool;
            public class Test {
              public void foo() {
                for (@AutoreleasePool int i = 0; i < 10; i++) {
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(
        translation,
        """
        for (int32_t i = 0; i < 10; i++) {
          @autoreleasepool {
          }
        }
        """);
  }

  public void testAutoreleasePoolEnhancedForStatement() throws IOException {
    String translation =
        translateSourceFile(
            """
            import com.google.j2objc.annotations.AutoreleasePool;
            public class Test {
              public void foo(String[] strings) {
                for (@AutoreleasePool String s : strings) {
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "@autoreleasepool");
  }

  public void testARCAutoreleasePoolForStatement() throws IOException {
    options.setMemoryManagementOption(MemoryManagementOption.ARC);
    String translation =
        translateSourceFile(
            """
            import com.google.j2objc.annotations.AutoreleasePool;
            public class Test {
              public void foo() {
                for (@AutoreleasePool int i = 0; i < 10; i++) {
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(
        translation,
        """
        for (int32_t i = 0; i < 10; i++) {
          @autoreleasepool {
          }
        }
        """);
  }

  public void testARCAutoreleasePoolEnhancedForStatement() throws IOException {
    options.setMemoryManagementOption(MemoryManagementOption.ARC);
    String translation =
        translateSourceFile(
            """
            import com.google.j2objc.annotations.AutoreleasePool;
            public class Test {
              public void foo(String[] strings) {
                for (@AutoreleasePool String s : strings) {
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "@autoreleasepool {");
  }

  public void testShiftAssignArrayElement() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test(int[] array) {
                int i = 2;
                array[0] >>>= 2;
                array[i - 1] >>>= 3;
                array[1] >>= 4;
                array[2] <<= 5;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "JreURShiftAssignInt(IOSIntArray_GetRef(nil_chk(array), 0), 2)");
    assertInTranslation(translation, "JreURShiftAssignInt(IOSIntArray_GetRef(array, i - 1), 3)");
    assertInTranslation(translation, "JreRShiftAssignInt(IOSIntArray_GetRef(array, 1), 4)");
    assertInTranslation(translation, "JreLShiftAssignInt(IOSIntArray_GetRef(array, 2), 5)");
  }

  public void testAssertWithoutDescription() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test() {
                int a = 5;
                int b = 6;
                assert a < b;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "JreAssert(a < b, @\"Test.java:5 condition failed: assert a < b;\")");
  }

  public void testAssertWithDescription() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test() {
                int a = 5;
                int b = 6;
                assert a < b : "a should be lower than b";
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "JreAssert(a < b, @\"a should be lower than b\")");
  }

  public void testAssertWithDynamicDescription() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test() {
                int a = 5;
                int b = 6;
                assert a < b : a + " should be lower than " + b;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "JreAssert(a < b, JreStrcat(\"I$I\", a, @\" should be lower than \", b));");
  }

  // Verify that a Unicode escape sequence is preserved with string
  // concatenation.
  public void testUnicodeStringConcat() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              static final String NAME = "\\u4e2d\\u56fd";
              static final String CAPTION = "China's name is ";
              static final String TEST = CAPTION + NAME;
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "Test_TEST = @\"China's name is \\u4e2d\\u56fd\"");
  }

  public void testPartialArrayCreation2D() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              void foo() {
                char[][] c = new char[3][];
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "#include \"IOSObjectArray.h\"");
    assertInTranslation(translation, "#include \"IOSClass.h\"");
    assertInTranslation(
        translation,
        "IOSObjectArray *c = [IOSObjectArray arrayWithLength:3 type:IOSClass_charArray(1)]");
  }

  public void testPartialArrayCreation3D() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              void foo() {
                char[][][] c = new char[3][][];
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "#include \"IOSObjectArray.h\"");
    assertInTranslation(
        translation,
        "IOSObjectArray *c = [IOSObjectArray arrayWithLength:3 type:IOSClass_charArray(2)]");
  }

  public void testUnsignedRightShift() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test(int a, long b, char c, byte d, short e) {
                long r;
                r = a >>> 1;
                r = b >>> 2;
                r = c >>> 3;
                r = d >>> 4;
                r = e >>> 5;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "r = JreURShift32(a, 1);");
    assertInTranslation(translation, "r = JreURShift64(b, 2);");
    assertInTranslation(translation, "r = JreURShift32(c, 3);");
    assertInTranslation(translation, "r = JreURShift32(d, 4);");
    assertInTranslation(translation, "r = JreURShift32(e, 5);");
  }

  public void testUnsignedRightShiftAssign() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test(int a, long b, char c, byte d, short e) {
                a >>>= 1;
                b >>>= 2;
                c >>>= 3;
                d >>>= 4;
                e >>>= 5;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "JreURShiftAssignInt(&a, 1);");
    assertInTranslation(translation, "JreURShiftAssignLong(&b, 2);");
    assertInTranslation(translation, "JreURShiftAssignChar(&c, 3);");
    assertInTranslation(translation, "JreURShiftAssignByte(&d, 4);");
    assertInTranslation(translation, "JreURShiftAssignShort(&e, 5);");
  }

  public void testUnsignedShiftRightAssignCharArray() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test(char[] array) {
                array[0] >>>= 2;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "JreURShiftAssignChar(IOSCharArray_GetRef(nil_chk(array), 0), 2)");
  }

  public void testDoubleQuoteConcatenation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              String test(String s) {
                return '"' + s + '"';
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return JreStrcat(\"C$C\", '\"', s, '\"');");
  }

  public void testIntConcatenation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void check(boolean expr, String fmt, Object... args) {}
              void test(int i, int j) {
                check(true, "%d-%d", i, j);
              }
            }
            """,
            "Test", "Test.m");
    assertInTranslation(
        translation,
        "[self checkWithBoolean:true withNSString:@\"%d-%d\" "
            + "withNSObjectArray:[IOSObjectArray arrayWithObjects:(id[]){ "
            + "JavaLangInteger_valueOfWithInt_(i), JavaLangInteger_valueOfWithInt_(j) } count:2 "
            + "type:NSObject_class_()]];");
  }

  public void testBinaryLiterals() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              byte aByte = (byte) 0b00100001;
              short aShort = (short) 0b1010000101000101;
              int anInt1 = 0b10100001010001011010000101000101;
              int anInt2 = 0b101;
              int anInt3 = 0B101; // b can be lower or upper case.
              long aLong = 0b1010000101000101101000010100010110100001010001011010000101000101L;
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "aByte_ = (int8_t) 0b00100001;");
    assertInTranslation(translation, "aShort_ = (int16_t) 0b1010000101000101;");
    assertInTranslation(translation, "anInt1_ = 0b10100001010001011010000101000101;");
    assertInTranslation(translation, "anInt2_ = 0b101;");
    assertInTranslation(translation, "anInt3_ = 0B101;");
    assertInTranslation(
        translation,
        "aLong_ = 0b1010000101000101101000010100010110100001010001011010000101000101LL;");
  }

  public void testUnderscoresInNumericLiterals() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class A {
              long creditCardNumber = 1234_5678_9012_3456L;
              long socialSecurityNumber = 999_99_9999L;
              float pi = 3.14_15F;
              long hexBytes = 0xFF_EC_DE_5E;
              long hexWords = 0xCAFE_BABE;
              long maxLong = 0x7fff_ffff_ffff_ffffL;
              byte nybbles = 0b0010_0101;
              long bytes = 0b11010010_01101001_10010100_10010010;
            }
            """,
            "A",
            "A.m");
    assertInTranslation(translation, "creditCardNumber_ = 1234567890123456LL;");
    assertInTranslation(translation, "socialSecurityNumber_ = 999999999LL;");
    assertInTranslation(translation, "pi_ = 3.1415f;");
    assertInTranslation(translation, "hexBytes_ = (int32_t) 0xFFECDE5E;");
    assertInTranslation(translation, "hexWords_ = (int32_t) 0xCAFEBABE;");
    assertInTranslation(translation, "maxLong_ = (int64_t) 0x7fffffffffffffffLL;");
    assertInTranslation(translation, "nybbles_ = 0b00100101;");
    assertInTranslation(translation, "bytes_ = 0b11010010011010011001010010010010;");
  }

  // Verify that the null literal is concatenated as "null" in strings.
  public void testNullConcatenation() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              String test(String s) {
                return "the nil value is " + null;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return JreStrcat(\"$@\", @\"the nil value is \", nil);");
  }

  public void testTypeVariableWithBoundsIsCast() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test<E> {
              interface A<T> {
                void foo();
              }
              class B {
                int foo() {
                  return 1;
                }
              }
              <T extends A<? super E>> void test(T t) {
                t.foo();
              }
              <T extends B> void test2(T t) {
                t.foo();
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "[((id<Test_A>) nil_chk(t)) foo];");
    assertInTranslation(translation, "[((Test_B *) nil_chk(t)) foo];");
  }

  public void testTypeVariableWithMultipleBounds() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test<T extends String & Runnable & Cloneable> {
              T t;
            }
            """,
            "Test",
            "Test.h");
    assertInTranslation(translation, "NSString<JavaLangRunnable, NSCopying> *t");
  }

  public void testMultiCatch() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.*;
            public class Test {
              static class FirstException extends Exception {}
              static class SecondException extends Exception {}
              public void rethrowException(String exceptionName) throws FirstException, SecondException {
                try {
                  if (exceptionName.equals("First")) {
                    throw new FirstException();
                  } else {
                    throw new SecondException();
                  }
                } catch (FirstException | SecondException e) {
                  throw e;
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "@catch (Test_FirstException *e) {\n    @throw e;\n  }");
    assertInTranslation(translation, "@catch (Test_SecondException *e) {\n    @throw e;\n  }");
    assertNotInTranslation(translation, "@catch (JavaLangException *e) {\n    @throw e;\n  }");
  }

  public void testLambdaCapturesMultiCatchExceptionParameter() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.function.Supplier;
            public class Test {
              public void test() {
                try {
                  "".charAt(10);
                } catch (ArrayIndexOutOfBoundsException | StringIndexOutOfBoundsException e) {
                  Supplier<String> s = () -> e.getMessage();
                  System.out.println(s.get());
                }
              }
            }
            """,
            "Test",
            "Test.m");
    // Note that the type of the captured parameter is the least upper bound of
    // (Array | String) IndexOutOfBoundsException.
    assertInTranslation(translation, "JavaLangIndexOutOfBoundsException *capture$0");
  }

  public void testDifferentTypesInConditionalExpression() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              String test(Runnable r) {
                return "foo" + (r != null ? r : "bar");
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "(r != nil ? r : (id) @\"bar\")");
  }

  // Verify that when a method invocation returns an object that is ignored,
  // it is cast to (void) to avoid a clang warning when compiling with ARC.
  public void testVoidedUnusedInvocationReturn() throws IOException {
    options.setMemoryManagementOption(MemoryManagementOption.ARC);
    String translation =
        translateSourceFile(
            """
            class Test {
              void test() {
                StringBuilder sb = new StringBuilder();
                sb.append("hello, world");
                new Throwable();
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "(void) [sb appendWithNSString:@\"hello, world\"];");
    assertInTranslation(translation, "(void) new_JavaLangThrowable_init();");
  }

  // Verify that multiple resources are closed in reverse order from opening.
  public void testTryMultiResourcesNoCatchOrFinally() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.io.*;
            public class Test {
              static class Resource implements AutoCloseable {
                public void close() throws Exception {}
              }
              void test() throws Exception {
                try (Resource r1 = new Resource();
                    Resource r2 = new Resource();
                    Resource r3 = new Resource()) {
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedSegments(
        translation, "Test_Resource *r1", "Test_Resource *r2", "Test_Resource *r3");
    assertTranslatedSegments(translation, "[r3 close]", "[r2 close]", "[r1 close]");
  }

  public void testGenericResultIsCastForChainedMethodCall() throws IOException {
    String translation =
        translateSourceFile(
            """
            abstract class Test<T extends Test.Foo> {
              abstract T getObj();
              static class Foo {
                void foo() {}
              }
              static void test(Test<Foo> t) {
                t.getObj().foo();
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "[((Test_Foo *) nil_chk([((Test *) nil_chk(t)) getObj])) foo]");
  }

  public void testCastResultWhenInterfaceDeclaresMoreGenericType() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              interface I1 {
                A foo();
              }
              interface I2 {
                B foo();
              }
              static class A {}
              static class B extends A {}
              static abstract class C {
                abstract B foo();
              }
              static abstract class D extends C implements I1, I2 {}
              B test(D d) {
                return d.foo();
              }
            }
            """,
            "Test",
            "Test.h");
    // Check that protocols are declared in the same order.
    assertInTranslation(translation, "@interface Test_D : Test_C < Test_I1, Test_I2 >");
    // A "foo" declaration is added to class "D" to override the less specific
    // return type inherited from "I1".
    assertOccurrences(translation, "- (Test_B *)foo;", 3);
    translation = getTranslatedFile("Test.m");
    // Check that the result of d.foo() is not cast.
    assertInTranslation(translation, "return [((Test_D *) nil_chk(d)) foo];");
  }

  public void testStaticMethodCalledOnObject() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              static void foo() {}
              void test(Test t) {
                t.foo();
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "Test_foo();");
  }

  public void testAnnotationVariableDeclaration() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              void test() {
                Deprecated annotation = null;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "id<JavaLangDeprecated> annotation = ");
  }

  public void testAnnotationTypeLiteral() throws IOException {
    String translation =
        translateSourceFile(
            """
            @Deprecated
            public class Test {
              Deprecated deprecated() {
                return Test.class.getAnnotation(Deprecated.class);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "JavaLangDeprecated_class_()");
  }

  public void testEnumThisCallWithNoArguments() throws IOException {
    String translation =
        translateSourceFile(
            """
            enum Test {
              A,
              B;
              Test() {}
              Test(int i) {
                this();
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "JavaLangEnum_initWithNSString_withInt_(self, __name, __ordinal);");
    // Called from the "this()" call.
    assertOccurrences(translation, "Test_initWithNSString_withInt_(self, __name, __ordinal);", 1);
  }

  public void testForStatementWithMultipleInitializers() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              void test() {
                for (String s1 = null, s2 = null; ; ) {}
              }
            }
            """,
            "Test",
            "Test.m");
    // C requires that each var have its own pointer.
    assertInTranslation(translation, "for (NSString *s1 = nil, *s2 = nil; ; )");
  }

  // Verify that constant variables are directly referenced when expression is "self".
  public void testSelfStaticVarAccess() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              enum Type {
                TYPE_BOOL;
              }
              Type test() {
                return Type.TYPE_BOOL;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "return JreLoadEnum(Test_Type, TYPE_BOOL);");
  }

  public void testMakeQuotedStringHang() throws IOException {
    // Test hangs if bug makeQuotedString() isn't fixed.
    translateSourceFile(
        """
        public class Test {
          void test(String s) {
            assert !"null\\foo\\nbar".equals(s);
          }
        }
        """,
        "Test",
        "Test.m");
  }

  // Verify that the type of superclass field's type variable is cast properly.
  public void testSuperTypeVariable() throws IOException {
    addSourceFile(
        """
        import java.util.List;
        class TestList <T extends List> {
          protected final T testField;
          TestList(T field) {
            testField = field;
          }
        }
        """,
        "TestList.java");
    addSourceFile(
        """
        import java.util.ArrayList;
        class TestArrayList extends TestList<ArrayList> {
          TestArrayList(ArrayList list) {
            super(list);
          }
        }
        """,
        "TestArrayList.java");
    String translation =
        translateSourceFile(
            """
            import java.util.ArrayList;
            class Test extends TestArrayList {
              Test(ArrayList list) {
                super(list);
              }
              private class Inner {
                void test() {
                  testField.ensureCapacity(42);
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation,
        "[((JavaUtilArrayList *) nil_chk(this$0_->testField_)) ensureCapacityWithInt:42];");
  }

  public void testNoTrigraphs() throws IOException {
    String translation =
        translateSourceFile(
            """
            // C trigraph list from http://en.wikipedia.org/wiki/Digraphs_and_trigraphs#C.
            class Test {
              static final String S1 = "??=??/??'??(??)??!??<??>??-";
              // S2 has char sequences that start with ?? but aren't trigraphs.
              static final String S2 = "??@??$??%??&??*??A??z??1??.";
            }
            """,
            "Test", "Test.m");
    assertInTranslation(
        translation,
        "S1 = @\"?\" \"?=?\" \"?/?\" \"?'?\" \"?(?\" \"?)?\" \"?!?\" \"?<?\" \"?>?\" \"?-\";");
    assertInTranslation(translation, "S2 = @\"??@??$??%??&??*??A??z??1??.\";");
  }

  // Verify that casting from a floating point primitive to an integral primitive
  // uses the right cast macro.
  public void testFloatingPointCasts() throws IOException {
    String translation =
        translateSourceFile(
            """
            public class Test {
              byte testByte(float f) {
                return (byte) f;
              }
              char testChar(float f) {
                return (char) f;
              }
              short testShort(float f) {
                return (short) f;
              }
              int testInt(float f) {
                return (int) f;
              }
              long testLong(float f) {
                return (long) f;
              }
              byte testByte(double d) {
                return (byte) d;
              }
              char testChar(double d) {
                return (char) d;
              }
              short testShort(double d) {
                return (short) d;
              }
              int testInt(double d) {
                return (int) d;
              }
              long testLong(double d) {
                return (long) d;
              }
            }
            """,
            "Test",
            "Test.m");
    // Verify referenced return value is cast.
    assertTranslatedLines(
        translation, "- (int8_t)testByteWithFloat:(float)f {", "return JreFpToByte(f);");
    assertTranslatedLines(
        translation, "- (unichar)testCharWithFloat:(float)f {", "return JreFpToChar(f);");
    assertTranslatedLines(
        translation, "- (int16_t)testShortWithFloat:(float)f {", "return JreFpToShort(f);");
    assertTranslatedLines(
        translation, "- (int32_t)testIntWithFloat:(float)f {", "return JreFpToInt(f);");
    assertTranslatedLines(
        translation, "- (int64_t)testLongWithFloat:(float)f {", "return JreFpToLong(f);");
    assertTranslatedLines(
        translation, "- (int8_t)testByteWithDouble:(double)d {", "return JreFpToByte(d);");
    assertTranslatedLines(
        translation, "- (unichar)testCharWithDouble:(double)d {", "return JreFpToChar(d);");
    assertTranslatedLines(
        translation, "- (int16_t)testShortWithDouble:(double)d {", "return JreFpToShort(d);");
    assertTranslatedLines(
        translation, "- (int32_t)testIntWithDouble:(double)d {", "return JreFpToInt(d);");
    assertTranslatedLines(
        translation, "- (int64_t)testLongWithDouble:(double)d {", "return JreFpToLong(d);");
  }

  // Verify that string constants used in switch statements can be generated after functionizing.
  public void testFunctionalizedStringStringStatement() throws IOException {
    String source =
        """
        class A {
          private static final String STR = "";
          private void f(String s) {
            switch (s) {
              case STR:
                return;
            }
          }
          public void g() {
            f("");
          }
        }
        """;
    // Assertion was thrown in StatementGenerator.getStringConstant(), due to the QualifiedName
    // node not having a constant value.
    translateSourceFile(source, "A", "A.m");
  }

  public void testSuppressedUnusedVariable() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              void test() {
                @SuppressWarnings("unused") int foo;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "__unused int32_t foo;");
  }

  public void testSuppressedUnusedVariableFromMethod() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              @SuppressWarnings("unused") void test() {
                int foo;
                }
              }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "__unused int32_t foo;");
  }

  public void testSuppressedUnusedVariableFromClass() throws IOException {
    String translation =
        translateSourceFile(
            """
            @SuppressWarnings("unused")
            class Test {
              void test() {
                int foo;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "__unused int32_t foo;");
  }

  public void testSuppressedUnusedVariableByName() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              void test() {
                int unusedFoo;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "__unused int32_t unusedFoo;");
  }

  // Verify that empty statements line offset to owning statement is preserved.
  public void testEmptyStatementFormatting() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              void foo(int a, int b) {
                if (a < b) ;
              }
              void bar(int c, int d) {
                if (c < d)
                  ;
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(translation, "if (a < b) ;");
    assertTranslatedLines(translation, "if (c < d)", ";");
  }

  @SuppressWarnings("StringConcatToTextBlock")
  public void testVarLocalVariables() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.ArrayList;
            import java.util.stream.Stream;
            class Test {
              Stream test() {
                var list = new ArrayList<String>();
                var stream = list.stream();
                return stream;
              }
            }
            """,
            "Test",
            "Test.m");
    // Verify correct type inference.
    assertInTranslation(translation, "JavaUtilArrayList *list = create_JavaUtilArrayList_init();");
    assertInTranslation(translation, "id<JavaUtilStreamStream> stream = [list stream];");
  }

  @SuppressWarnings("StringConcatToTextBlock")
  public void testVarLambdaExpressionParameter() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.function.Function;
            class Test {
              int test(String input) {
                Function<String, Integer> f = (var s) -> s.length();
                return f.apply(input);
              }
            }
            """,
            "Test",
            "Test.m");
    assertInTranslation(
        translation, "@interface Test_$Lambda$1 : NSObject < JavaUtilFunctionFunction >");
  }

  @SuppressWarnings("StringConcatToTextBlock")
  public void testInstanceOfPatternVariableTranslation() throws IOException {
    String translation =
        translateSourceFile(
            """
            class Test {
              int test(Object o) {
                if (o instanceof String s) {
                  return s.length();
                }
                return 0;
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(
        translation,
        """
        NSString *s = nil;
        if ([o isKindOfClass:[NSString class]] && (s = (NSString *) o, true)) {
          return [((NSString *) nil_chk(s)) java_length];
        }
        return 0;
        """);
  }

  public void testInstanceOfPatternVariableTranslationWithGuards() throws IOException {
    String translation =
        translateSourceFile(
            // Define instanceof pattern variable p with two guards.
            """
            class Point {
              private final int x;
              private final int y;
              Point(int x, int y) { this.x = x; this.y = y; }
              @Override public boolean equals(Object o) {
                if (o instanceof Point p && x == p.x && y == p.y) {
                  return true;
                } else {
                  return false;
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(
        translation,
        """
        Point *p = nil;
        if ([o isKindOfClass:[Point class]] && (p = (Point *) o, true) && x_ == ((Point *) nil_chk(p))->x_ && y_ == p->y_) {
          return true;
        }
        else {
          return false;
        }
        """);
  }

  public void testSimpleSwitchExpression() throws IOException {
    String translation = translateSourceFile(SIMPLE_SWITCH_EXPRESSION, "Test", "Test.m");
    assertTranslatedLines(
        translation,
        """
        return ^ NSString * (){
          switch (k) {
            case 1:
            return @"one";
            case 2:
            return @"two";
            default:
            return @"many";
          }
        }();
        """);
  }

  // Verify converted switch expression's AST looks correct. This isolates
  // the TreeConverter from later translation steps.
  public void testASTConversionSimpleSwitchExpressionWithPatternAndGuard() throws IOException {
    // Switch expression patterns introduced in Java 21.
    String ast = translateType("Test", SIMPLE_SWITCH_EXPRESSION_WITH_PATTERN_AND_GUARD).toString();
    assertTranslatedLines(
        ast,
        """
        java.lang.String test(  java.lang.String str){
          java.lang.String msg=^{
            {
              java.lang.String s=null;
              java.lang.String s=null;
              int selector=0;
              if (str instanceof java.lang.String && (s=str, true) && ((java.lang.String)nil_chk(s)).length() > 10)         selector=1;
              else if (str == null)         selector=2;
              else if (str instanceof java.lang.String && (s=str, true))         selector=3;
              switch (selector) {
                case 1: return JreStrcat($$, "Long string: ", s);
                case 2: return "null";
                case 3: return JreStrcat($$, "Short string: ", s);
                default: __builtin_unreachable();
              };
            }
          }()
        ;
        """);
  }

  public void testNullSwitchExpressionCase() throws IOException {
    String translation =
        translateSourceFile(
                """
                class Test {
                  String testNullCase(String s) {
                    String result = switch (s) {
                      case null -> "oops";
                      case "Foo", "Bar" -> "great";
                      default -> "okay";
                    };
                    return result;
                  }
                }
                """,
                "Test",
                "Test.m")
            .toString();
    assertTranslatedLines(
        translation,
        """
        switch (JreIndexOfStr(s, (id[]){ nil, @\"Foo\", @\"Bar\" }, 3)) {
          case 0:
            return @"oops";
          case 1:
          case 2:
            return @"great";
          default:
            return @"okay";
        }
        """);
  }

  // Test from https://openjdk.org/jeps/441: Improved enum constant case labels
  public void testQualifiedEnumNamesInSwitchExpressionCase() throws IOException {
    String translation =
        translateSourceFile(
            """
            sealed interface Currency permits Coin {}
            enum Coin implements Currency { HEADS, TAILS }
            class Test {
              void goodEnumSwitch1(Currency c) {
                switch (c) {
                  case Coin.HEADS -> {
                    System.out.println("Heads");
                  }
                  case Coin.TAILS -> {
                    System.out.println("Tails");
                  }
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(
        translation,
        """
        - (void)goodEnumSwitch1WithCurrency:(id<Currency>)c {
          switch (c) {
            case Coin_Enum_HEADS:
            {
              [JreLoadStatic(JavaLangSystem, out) printlnWithNSString:@"Heads"];
              break;
            }
            case Coin_Enum_TAILS:
            {
              [JreLoadStatic(JavaLangSystem, out) printlnWithNSString:@"Tails"];
              break;
            }
          }
        }
        """);
  }

  public void testUnnamedVariableDeclaration() throws IOException {
    testOnJava22OrAbove(
        () -> {
          String translation =
              translateSourceFile(
                  """
                  import java.util.Date;
                  class Test {
                    private Date currentDate = null;
                    Date getCurrentDate() {
                      currentDate = new Date();
                    return currentDate;
                    }
                    void test() {
                      Date _ = getCurrentDate();
                      Number _ = null;
                      System.out.println("field initialized: " + (currentDate != null));
                    }
                  }
                  """,
                  "Test",
                  "Test.m");
          // Verify that unnamed local variable are named "_", as javac uses an empty strings. Also
          // verify that if there are multiple unnamed variables, they are not renamed.
          assertTranslatedLines(
              translation,
              "JavaUtilDate *_ = JreRetainedLocalValue([self getCurrentDate]);",
              "NSNumber *_ = nil;");
        });
  }

  public void testUnnamedSwitchCaseVariable() throws IOException {
    testOnJava22OrAbove(
        () -> {
          String translation =
              translateSourceFile(
                  """
                  class Test {
                    void test(Object obj) {
                      switch (obj) {
                        case Integer _ -> System.out.println("Is an integer");
                        case Float _ -> System.out.println("Is a float");
                        case String _ -> System.out.println("Is a String");
                        default -> System.out.println("Default");
                      }
                    }
                  }
                  """,
                  "Test",
                  "Test.m");
          // Verify that the local variable is named "_", as javac uses an empty strings.
          assertTranslatedLines(
              translation,
              """
              int32_t selector = 0;
              if ([obj isKindOfClass:[JavaLangInteger class]]) selector = 1;
              else if ([obj isKindOfClass:[JavaLangFloat class]]) selector = 2;
              else if ([obj isKindOfClass:[NSString class]]) selector = 3;
              switch (selector) {
                case 1:
                {
                  [JreLoadStatic(JavaLangSystem, out) printlnWithNSString:@"Is an integer"];
                  break;
                }
                case 2:
                {
                  [JreLoadStatic(JavaLangSystem, out) printlnWithNSString:@"Is a float"];
                  break;
                }
                case 3:
                {
                  [JreLoadStatic(JavaLangSystem, out) printlnWithNSString:@"Is a String"];
                  break;
                }
                default:
                {
                  [JreLoadStatic(JavaLangSystem, out) printlnWithNSString:@"Default"];
                  break;
                }
              }
              """);
        });
  }

  @SuppressWarnings("StringConcatToTextBlock")
  public void testUnnamedExceptionCatch() throws IOException {
    testOnJava22OrAbove(
        () -> {
          String translation =
              translateSourceFile(
                  """
                  import java.sql.*;
                  class Test {
                    void test(String url, String user, String pwd) {
                      try {
                        Connection _ = DriverManager.getConnection(url, user, pwd);
                        System.out.println("DB Connection successful");
                      } catch (SQLException e) {
                        System.err.println("Exception " + e);
                      }
                    }
                  }
                  """,
                  "Test",
                  "Test.m");
          // Verify that the local variable is named "_", as javac uses an empty strings.
          assertInTranslation(
              translation,
              "id<JavaSqlConnection> _ = JavaSqlDriverManager_getConnectionWithNSString_"
                  + "withNSString_withNSString_(url, user, pwd);");
        });
  }

  @SuppressWarnings("StringConcatToTextBlock")
  public void testUnnamedTryVariable() throws IOException {
    testOnJava22OrAbove(
        () -> {
          String translation =
              translateSourceFile(
                  """
                  import java.sql.*;
                  class Test {
                    void test(String url, String user, String pwd) {
                      try (Connection _ = DriverManager.getConnection(url, user, pwd)) {
                        System.out.println("DB Connection successful");
                      } catch (SQLException e) {
                        System.err.println("Exception " + e);
                      }
                    }
                  }
                  """,
                  "Test",
                  "Test.m");
          // Verify that the local variable is named "_", as javac uses an empty strings.
          assertInTranslation(
              translation,
              "id<JavaSqlConnection> _ = JavaSqlDriverManager_getConnectionWithNSString_"
                  + "withNSString_withNSString_(url, user, pwd);");
        });
  }

  @SuppressWarnings("StringConcatToTextBlock")
  public void testUnnamedInstanceofPatternVariable() throws IOException {
    testOnJava22OrAbove(
        () -> {
          String translation =
              translateSourceFile(
                  """
                  class Test {
                    void test(Object obj) {
                      boolean b = obj instanceof String _;
                    }
                  }
                  """,
                  "Test",
                  "Test.m");
          // Verify that the local variable is named "_", as javac uses an empty strings.
          assertInTranslation(translation, "bool b = [obj isKindOfClass:[NSString class]];");
        });
  }

  public void testAnyRecordPattern() throws IOException {
    testOnJava22OrAbove(
        () -> {
          String translation =
              translateSourceFile(
                  """
                  class Test {
                    record B(Object a1, int i, int i2) {}
                    void test() {
                      Object o = new B(null, 3, 4);
                      if (o instanceof B(_, _, var i2)) {
                      }
                    }
                  }
                  """,
                  "Test",
                  "Test.m");
          assertTranslatedLines(
              translation,
              """
              Test_B *rec = nil;
              int32_t i2 = 0;
              id o = create_Test_B_initWithId_withInt_withInt_(nil, 3, 4);
              if ([o isKindOfClass:[Test_B class]] && (rec = (Test_B *) o, true)\
               && (i2 = (int32_t) [((Test_B *) nil_chk(rec)) i2], true)) {
              }
              """);
        });
  }
}
